package command

import (
	"context"
	"strings"
	"time"

	"github.com/pkg/errors"
	agentv1beta1 "gopkg.openfuyao.cn/bkeagent/api/v1beta1"
	agentutils "gopkg.openfuyao.cn/bkeagent/utils"
	"gopkg.openfuyao.cn/bkeagent/utils/log"
	bkenode "gopkg.openfuyao.cn/bkecommon/cluster/node"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
	"sigs.k8s.io/cluster-api/util/patch"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/constant"
	l "gopkg.openfuyao.cn/cluster-api-provider-bke/utils/log"
)

type BaseCommand struct {
	Ctx         context.Context
	Client      client.Client
	NameSpace   string
	Scheme      *runtime.Scheme
	OwnerObj    metav1.Object
	ClusterName string

	// RemoveAfterWait remove the command after wait
	RemoveAfterWait bool
	// Unique only one command can be stored in the cluster
	Unique bool
	// ForceRemove force remove the command
	ForceRemove bool

	// WaitTimeout the command wait timeout
	WaitTimeout time.Duration
	// WaitInterval the command wait interval
	WaitInterval time.Duration

	commandName string
	// Command the command obj
	Command *agentv1beta1.Command
}

const (
	DefaultWaitTimeout  = 5 * time.Minute
	DefaultWaitInterval = 2 * time.Second
)

func (b *BaseCommand) validate() error {
	if b.Client == nil {
		return errors.New("client is nil")
	}
	if b.Scheme == nil {
		return errors.New("scheme is nil")
	}
	if b.NameSpace == "" {
		return errors.New("name space is empty")
	}
	return nil
}

type Command interface {
	// Validate the command fields
	Validate() error
	// New create the command obj
	New() error
}

const (
	// BootstrapCommandNamePrefix all the bootstrap command Prefix,
	BootstrapCommandNamePrefix = "bootstrap-"

	// HACommandName the loadbalancer command name,
	HACommandName = "k8s-ha-deploy"

	// K8sEnvCommandName the k8s env command name,
	K8sEnvCommandName = "k8s-env-init"

	// K8sContainerdResetCommandName the k8s containerd reset command name,
	K8sContainerdResetCommandName = "k8s-containerd-reset"

	// K8sContainerdRedeployCommandName the k8s containerd redeploy command name,
	K8sContainerdRedeployCommandName = "k8s-containerd-redeploy"

	// K8sEnvDryRunCommandName the k8s env command name,
	K8sEnvDryRunCommandName = "k8s-env-dry-run"

	// K8sHostsCommandName the k8s hosts command name,
	K8sHostsCommandName = "k8s-hosts-generate"

	// K8sImagePrePullCommandName the k8s image pre pull command name,
	K8sImagePrePullCommandName = "k8s-image-pre-pull"

	// SwitchClusterCommandNamePrefix the switch cluster command name prefix,
	SwitchClusterCommandNamePrefix = "switch-cluster-"

	ResetNodeCommandNamePrefix = "reset-node-"

	UpgradeNodeCommandNamePrefix = "upgrade-node-"

	PingCommandNamePrefix = "ping-"

	CollectCertCommandNamePrefix = "collect-"
)

// two label used to select the Command,when reconcile
const (
	BKEClusterLabel = "bke.bocloud.com/cluster-command"
	BKEMachineLabel = "bke.bocloud.com/machine-command"

	MasterInitCommandLabel = "bke.bocloud.com/master-init-command"
	MasterJoinCommandLabel = "bke.bocloud.com/master-join-command"
	WorkerJoinCommandLabel = "bke.bocloud.com/worker-join-command"
)

const (
	// DefaultBackoffLimit max retry times
	DefaultBackoffLimit            = 3
	DefaultActiveDeadlineSecond    = 1000
	DefaultTTLSecondsAfterFinished = 600
)

func (b *BaseCommand) newCommand(commandName, labelKey string, commandSpec *agentv1beta1.CommandSpec, customLabel ...string) error {
	if b.Unique {
		// 去掉字符串的时间戳
		commandNamePrefix := utils.RemoveTimestamps(commandName)
		// get all the command
		commandList := &agentv1beta1.CommandList{}
		if err := b.Client.List(b.Ctx, commandList, client.InNamespace(b.NameSpace)); err != nil {
			return errors.Wrapf(err, "failed to list command in namespace %s", b.NameSpace)
		}
		// delete the command
		for _, command := range commandList.Items {
			if strings.HasPrefix(command.Name, commandNamePrefix) {
				if err := b.Client.Delete(b.Ctx, &command); err != nil {
					return errors.Wrapf(err, "failed to delete command %s", command.Name)
				}
				break
			}
		}
	}

	b.setCommandName(commandName)
	command := &agentv1beta1.Command{}
	command.SetGroupVersionKind(agentv1beta1.GroupVersion.WithKind("Command"))
	command.SetName(commandName)
	command.SetNamespace(b.NameSpace)
	command.Spec = *commandSpec.DeepCopy()
	// set one label used to distinguish
	labels := map[string]string{
		labelKey: "",
	}
	if b.ClusterName != "" {
		labels[clusterv1.ClusterNameLabel] = b.ClusterName
	}
	for _, label := range customLabel {
		labels[label] = ""
	}
	command.SetLabels(labels)
	// set owner reference
	if b.OwnerObj != nil {
		if err := controllerutil.SetControllerReference(b.OwnerObj, command, b.Scheme); err != nil {
			return errors.Wrapf(err, "failed to set controller reference for owner %s, controlled %s", b.OwnerObj.GetName(), command.Name)
		}
	}
	if err := b.Client.Create(b.Ctx, command); err != nil {
		if apierrors.IsAlreadyExists(err) {
			_, err := b.GetCommand()
			return err
		}
		return errors.Wrapf(err, "failed to create command %s", command.Name)
	}
	return nil
}

func (b *BaseCommand) setCommandName(commandName string) {
	b.commandName = commandName
}

func (b *BaseCommand) GetCommand() (*agentv1beta1.Command, error) {
	if b.commandName == "" {
		return nil, errors.New("command name is empty")
	}
	command := &agentv1beta1.Command{}
	if err := b.Client.Get(b.Ctx, client.ObjectKey{Namespace: b.NameSpace, Name: b.commandName}, command); err != nil {
		return nil, errors.Wrapf(err, "failed to get command %s", b.commandName)
	}
	b.Command = command
	return command, nil
}

func (b *BaseCommand) deleteCommand() error {
	if b.Command == nil {
		return nil
	}
	if b.ForceRemove {
		patchHelper, err := patch.NewHelper(b.Command, b.Client)
		if err == nil {
			// remove finalizer and delete
			controllerutil.RemoveFinalizer(b.Command, "command.bkeagent.bocloud.com/finalizers")
			if err := patchHelper.Patch(b.Ctx, b.Command); err != nil {
				log.Warn(constant.ReconcileErrorReason, "failed to remove finalizer: %v", err)
			}
		} else {
			log.Warn(constant.ReconcileErrorReason, "failed to create patch helper: %v", err)
		}
	}

	if err := b.Client.Delete(b.Ctx, b.Command); err != nil {
		return errors.Wrapf(err, "failed to update command %s", b.Command.Name)
	}
	return nil
}

func (b *BaseCommand) waitCommandComplete() (error, bool, []string, []string) {
	if b.commandName == "" {
		return errors.New("command name is empty"), false, nil, nil
	}
	if b.WaitInterval == 0 {
		b.WaitInterval = DefaultWaitInterval
	}
	if b.WaitTimeout == 0 {
		b.WaitTimeout = DefaultWaitTimeout
	}

	var complete bool
	var successNodes []string
	var failedNodes []string

	ctxTimeout, cancel := context.WithTimeout(b.Ctx, b.WaitTimeout)
	defer cancel()
	err := wait.PollImmediateUntil(b.WaitInterval, func() (bool, error) {
		command, err := b.GetCommand()
		if err != nil {
			return false, nil
		}
		complete, successNodes, failedNodes = CheckCommandStatus(command)
		if complete {
			return true, nil
		}
		return false, nil
	}, ctxTimeout.Done())

	if errors.Is(err, wait.ErrWaitTimeout) {
		// 不返回错误，如果是超时
		err = nil
		// 获取未完成的node
		command, err := b.GetCommand()
		if err != nil {
			return err, complete, successNodes, failedNodes
		}
		successNodesCopy := make([]string, len(successNodes))
		copy(successNodesCopy, successNodes)

		for key := range command.Spec.NodeSelector.MatchLabels {
			found := false
			for i, node := range successNodesCopy {
				if strings.Contains(node, key) {
					found = true
					successNodesCopy = append(successNodesCopy[:i], successNodesCopy[i+1:]...)
				}
			}
			if !found {
				failedNodes = append(failedNodes, key)
			}
		}
	}

	if b.RemoveAfterWait {
		if err := b.deleteCommand(); err != nil {
			l.Warnf("delete command %s failed: %v", b.commandName, err)
		}
	}
	return err, complete, successNodes, failedNodes
}

// ClusterNameLabelSelectorRequirement return the label selector requirement
// Deprecated
func (b *BaseCommand) ClusterNameLabelSelectorRequirement() metav1.LabelSelectorRequirement {
	return metav1.LabelSelectorRequirement{
		Key:      agentutils.ClusterNameLabelKey,
		Operator: metav1.LabelSelectorOpIn,
		Values:   []string{b.ClusterName},
	}
}

func GenerateDefaultCommandSpec() *agentv1beta1.CommandSpec {
	return &agentv1beta1.CommandSpec{
		NodeName:                "",
		Suspend:                 false,
		Commands:                []agentv1beta1.ExecCommand{},
		BackoffLimit:            DefaultBackoffLimit,
		ActiveDeadlineSecond:    DefaultActiveDeadlineSecond,
		TTLSecondsAfterFinished: DefaultTTLSecondsAfterFinished,
		NodeSelector:            &metav1.LabelSelector{},
	}
}

// ValidateCommand validate the command
func ValidateCommand(c *agentv1beta1.Command) error {
	if c.Spec.NodeName == "" && c.Spec.NodeSelector.String() == "" {
		return errors.New("not a valid command,at least provide a node name or NodeSelector")
	}
	// todo: validate this command
	return nil
}

// CheckCommandStatus check all the command is success or not at agentv1beta1.Status
// if all command exec completed,return true , successNodes and failedNodes
// else return false
func CheckCommandStatus(c *agentv1beta1.Command) (complete bool, successNodes []string, failedNodes []string) {
	if len(c.Status) == 0 || c.Status == nil {
		return
	}

	complete = true
	if c.Spec.Suspend {
		complete = false
		return
	}
	count := 0

	for nodeName, commandStatus := range c.Status {
		count++
		switch {
		case commandStatus.Phase == agentv1beta1.CommandSuspend:
			complete = false
			return
		case commandStatus.Phase == agentv1beta1.CommandRunning:
			complete = false
			return
		case commandStatus.Phase == agentv1beta1.CommandFailed:
			failedNodes = append(failedNodes, nodeName)
		case commandStatus.Status != metav1.ConditionTrue:
			failedNodes = append(failedNodes, nodeName)
		case commandStatus.Failed > 0:
			failedNodes = append(failedNodes, nodeName)
		// todo add more case
		default:
			successNodes = append(successNodes, nodeName)
		}
	}
	if len(successNodes)+len(failedNodes) != count {
		complete = false
	}

	if count == 0 || len(c.Spec.NodeSelector.MatchLabels) != count {
		complete = false
	}

	return
}

// IsOwnerRefCommand returns a bool ,if object is owner of the command
func IsOwnerRefCommand(object metav1.Object, command agentv1beta1.Command) bool {
	for _, ref := range command.GetOwnerReferences() {
		if ref.UID == object.GetUID() {
			return true
		}
	}
	return false
}

func getNodeSelector(nodes bkenode.Nodes) *metav1.LabelSelector {
	nodeSelector := &metav1.LabelSelector{}
	for _, node := range nodes {
		metav1.AddLabelToSelector(nodeSelector, node.IP, node.IP)
	}
	return nodeSelector
}
